/*
 * Copyright (c) 2025 Huawei Device Co., Ltd.
 * Licensed under the Apache License, Version 2.0 (the 'License');
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
import {ArkFile, AstTreeUtils, ts} from 'arkanalyzer';
import Logger, { LOG_MODULE_TYPE } from 'arkanalyzer/lib/utils/logger';
import { BaseChecker, BaseMetaData } from '../BaseChecker';
import { Defects } from '../../Index';
import { FileMatcher, MatcherCallback, MatcherTypes } from '../../Index';
import { Rule } from '../../Index';
import { RuleListUtil } from '../../utils/common/DefectsList';
import { IssueReport } from '../../model/Defects';

const logger = Logger.getLogger(LOG_MODULE_TYPE.HOMECHECK, 'NoShadowCheck');

const gMetaData: BaseMetaData = {
    severity: 2,
    ruleDocPath: 'docs/no-shadow-check.md',
    description: 'Disallow variable declarations from shadowing variables declared in the outer scope.'
};

type RuleOptions = {
    ignoreTypeValueShadow: boolean,
    ignoreFunctionTypeParameterNameValueShadow: boolean
}

type CheckMember = {
    name: string;
    node: ts.Node
};

export class NoShadowCheck implements BaseChecker {
    readonly metaData: BaseMetaData = gMetaData;
    public rule: Rule;
    public defects: Defects[] = [];
    public issues: IssueReport[] = [];
    private ruleOptions: RuleOptions = { ignoreTypeValueShadow: true, ignoreFunctionTypeParameterNameValueShadow: true }

    private fileMatcher: FileMatcher = {
        matcherType: MatcherTypes.FILE
    };

    public registerMatchers(): MatcherCallback[] {
        const fileMatchBuildCb: MatcherCallback = {
            matcher: this.fileMatcher,
            callback: this.check
        }
        return [fileMatchBuildCb];
    }

    public check = (targetFile: ArkFile): void => {
        let options = this.rule.option;
        if (options.length > 0) {
            this.ruleOptions = options[0] as RuleOptions;
        }

        const sourceFile = AstTreeUtils.getASTNode(targetFile.getName(), targetFile.getCode());
        const sourceFileObject = ts.getParseTreeNode(sourceFile);
        if (sourceFileObject === undefined) {
            return;
        }

        this.loopNode(targetFile, sourceFile, sourceFileObject);
    }

    public loopNode(targetFile: ArkFile, sourceFile: ts.SourceFileLike, aNode: ts.Node): void {
        const children = aNode.getChildren();
        for (const child of children) {
            if (this.isObjectGlobal(child)) {
                continue;
            }

            this.checkObject(targetFile, sourceFile, child);

            this.loopNode(targetFile, sourceFile, child);
        }
    }

    private checkObject(targetFile: ArkFile, sourceFile: ts.SourceFileLike, aNode: ts.Node): void {
        const member = this.getCheckObject(targetFile, sourceFile, aNode);
        if (member) {
            this.checkShadow(targetFile, sourceFile, member);
        }
    }

    // 获取需要检查的对象
    private getCheckObject(targetFile: ArkFile, sourceFile: ts.SourceFileLike, aNode: ts.Node): CheckMember | undefined {
        let member: CheckMember | undefined = undefined;
        if (ts.isParameter(aNode)) {
            if (!this.ruleOptions.ignoreFunctionTypeParameterNameValueShadow) {
                member = this.getBindingNameNode(aNode.name);
            }
        } else if (ts.isVariableDeclaration(aNode)) {
            member = this.getBindingNameNode(aNode.name);
        } else if (ts.isTypeParameterDeclaration(aNode)) {
            member = this.getBindingNameNode(aNode.name);
        } else if (ts.isTypeAliasDeclaration(aNode)) {
            member = this.getBindingNameNode(aNode.name);
        } else if (ts.isEnumMember(aNode)) {
            member = this.getPropertyNameNode(aNode.name);
        } else if (ts.isInterfaceDeclaration(aNode)) {
            member = this.getBindingNameNode(aNode.name);
        }
        return member;
    }

    private getPropertyNameNode(nameNode: ts.PropertyName): CheckMember | undefined {
        let member: CheckMember | undefined = undefined;

        if (ts.isIdentifier(nameNode)) {
            if (nameNode.escapedText) {
                if (nameNode.escapedText !== 'this') {
                    member = { name: nameNode.escapedText, node: nameNode };
                }
            }
        } else if (ts.isStringLiteral(nameNode)) {
            member = { name: nameNode.text, node: nameNode };
        } else if (ts.isNumericLiteral(nameNode)) {
            member = { name: nameNode.text, node: nameNode };
        } else if (ts.isComputedPropertyName(nameNode)) {
            member = { name: nameNode.getText(), node: nameNode };
        } else if (ts.isPrivateIdentifier(nameNode)) {
            if (nameNode.escapedText) {
                member = { name: nameNode.escapedText, node: nameNode };
            }
        }

        return member;
    }

    private getBindingNameNode(nameNode: ts.BindingName): CheckMember | undefined {
        let member: CheckMember | undefined = undefined;

        if (ts.isIdentifier(nameNode)) {
            if (nameNode.escapedText) {
                if (nameNode.escapedText !== 'this') {
                    member = { name: nameNode.escapedText, node: nameNode };
                }
            }
        } else if (ts.isArrayBindingPattern(nameNode)) {
            for (const element of nameNode.elements) {
                if (ts.isBindingElement(element)) {
                    member = { name: element.name.getText(), node: element.name };
                }
            }
        } else if (ts.isObjectBindingPattern(nameNode)) {
            for (const element of nameNode.elements) {
                if (ts.isBindingElement(element)) {
                    member = { name: element.name.getText(), node: element.name };
                }
            }
        }

        return member;
    }

    private isObjectGlobal(aNode: ts.Node): boolean {
        if (ts.isModuleDeclaration(aNode)) {
            const children = aNode.getChildren();
            if (children.length >= 2) {
                if (children[0].getText() === 'declare' && children[1].getText() === 'global') {
                    return true;
                }
            }
        }
        return false;
    }

    private checkShadow(targetFile: ArkFile, sourceFile: ts.SourceFileLike, aNode: CheckMember): void {
        this.checkShadowInParent(targetFile, sourceFile, aNode, aNode.node);
    }

    private checkShadowInParent(targetFile: ArkFile, sourceFile: ts.SourceFileLike, sourceNode: CheckMember, currentNode: ts.Node): void {
        const parentNode = currentNode.parent;
        if (parentNode === undefined) {
            return;
        }

        let parentsNode: ts.Node[] = []; // 父节点和父节点的兄弟节点
        const parentParentNode = parentNode.parent;
        if (parentParentNode === undefined) {
            parentsNode.push(parentNode);
        } else {
            parentsNode = parentsNode.concat(parentParentNode.getChildren());
        }

        let parents: ts.Node[] = [];
        for (const node of parentsNode) {
            if (ts.isToken(node)) {
                continue;
            }
            parents.push(node);
        }

        for (const parent of parents) {
            this.checkShadowInNodeChildren(targetFile, sourceFile, sourceNode, parent);
        }

        this.checkShadowInParent(targetFile, sourceFile, sourceNode, parentNode);
    }

    // 在这个节点的子节点内查找指定名称的节点
    private checkShadowInNodeChildren(targetFile: ArkFile, sourceFile: ts.SourceFileLike, sourceNode: CheckMember, currentNode: ts.Node): boolean {
        if (currentNode.kind === ts.SyntaxKind.SyntaxList) {
            for (const child of currentNode.getChildren()) {
                if (this.checkShadowWithNode(targetFile, sourceFile, sourceNode, child)) {
                    return true;
                }
            }
        } else {
            if (this.checkShadowWithNode(targetFile, sourceFile, sourceNode, currentNode)) {
                return true;
            }
        }
        return false;
    }

    private checkShadowWithNode(targetFile: ArkFile, sourceFile: ts.SourceFileLike, sourceNode: CheckMember, aNode: ts.Node): boolean {
        if (ts.isParameter(aNode)) {
            this.isParameter(targetFile, sourceFile, sourceNode, aNode);
        } else if (ts.isTypeAliasDeclaration(aNode) || ts.isInterfaceDeclaration(aNode)) {
            this.isTypeAliasDeclaration(targetFile, sourceFile, sourceNode, aNode);
        } else if (ts.isVariableDeclaration(aNode)) {
            this.isVariableDeclaration(targetFile, sourceFile, sourceNode, aNode);
        } else if (ts.isVariableStatement(aNode)) {
            this.isVariableStatement(targetFile, sourceFile, sourceNode, aNode);
        } else if (ts.isImportDeclaration(aNode)) {
            this.isImportDeclaration(targetFile, sourceFile, sourceNode, aNode);
        }
        return false;
    }

    private isParameter(targetFile: ArkFile, sourceFile: ts.SourceFileLike, sourceNode: CheckMember, aNode: ts.Node): boolean {
        if (!this.ruleOptions.ignoreFunctionTypeParameterNameValueShadow) {
            if (aNode.getText() === sourceNode.name) {
                if (this.reportShadow(targetFile, sourceFile, sourceNode.node, aNode)) {
                    return true;
                }
            }
            return false;
        }
        return false;
    }

    private isTypeAliasDeclaration(targetFile: ArkFile, sourceFile: ts.SourceFileLike, sourceNode: CheckMember, aNode: ts.Node): boolean {
        if (!this.ruleOptions.ignoreTypeValueShadow) {
            if (aNode && aNode.getText() === sourceNode.name) {
                if (this.reportShadow(targetFile, sourceFile, sourceNode.node, aNode)) {
                    return true;
                }
            }
        }
        return false;
    }

    private isVariableDeclaration(targetFile: ArkFile, sourceFile: ts.SourceFileLike, sourceNode: CheckMember, aNode: ts.Node): boolean {
        if (aNode.getText() === sourceNode.name) {
            if (this.reportShadow(targetFile, sourceFile, sourceNode.node, aNode)) {
                return true;
            }
            return false;
        }
        return false;
    }

    private isVariableStatement(targetFile: ArkFile, sourceFile: ts.SourceFileLike, sourceNode: CheckMember, aNode: ts.Node): boolean {
        if (!ts.isVariableStatement(aNode)) {
            return false;
        }
        for (let declaration of aNode.declarationList.declarations) {
            if (declaration.name.getText() === sourceNode.name) {
                if (this.reportShadow(targetFile, sourceFile, sourceNode.node, declaration.name)) {
                    return true;
                }
            }
        }
        return false;
    }

    private isImportDeclaration(targetFile: ArkFile, sourceFile: ts.SourceFileLike, sourceNode: CheckMember, aNode: ts.Node): boolean {
        if (!ts.isImportDeclaration(aNode)) {
            return false;
        }
        if (aNode.importClause) {
            const ignoreNode = this.ruleOptions.ignoreTypeValueShadow && aNode.importClause.isTypeOnly;
            this.ignoreNode(ignoreNode, targetFile, sourceFile, sourceNode, aNode);
        }
        return false;
    }

    /**
     * 处理导入声明的忽略节点
     */
    private ignoreNode(
        ignoreTypeOnly: boolean,
        targetFile: ArkFile,
        sourceFile: ts.SourceFileLike,
        sourceNode: CheckMember,
        importDecl: ts.Node
    ): boolean {
        if (!ts.isImportDeclaration(importDecl) || !importDecl.importClause) {
            return false;
        }

        // 如果不忽略类型导入，则检查命名导入
        if (!ignoreTypeOnly) {
            return this.checkNamedImports(
                targetFile,
                sourceFile,
                sourceNode,
                importDecl.importClause
            );
        }

        return false;
    }

    /**
     * 检查命名导入
     */
    private checkNamedImports(
        targetFile: ArkFile,
        sourceFile: ts.SourceFileLike,
        sourceNode: CheckMember,
        importClause: ts.ImportClause
    ): boolean {
        for (const child of importClause.getChildren()) {
            if (ts.isToken(child) || !ts.isNamedImports(child)) {
                continue;
            }

            return this.processNamedImportElements(
                targetFile,
                sourceFile,
                sourceNode,
                child.elements
            );
        }

        return false;
    }

    /**
     * 处理命名导入元素
     */
    private processNamedImportElements(
        targetFile: ArkFile,
        sourceFile: ts.SourceFileLike,
        sourceNode: CheckMember,
        elements: ts.NodeArray<ts.ImportSpecifier>
    ): boolean {
        for (const namedImport of elements) {
            // 如果配置了忽略类型值阴影且当前导入是类型导入，则跳过
            if (this.ruleOptions.ignoreTypeValueShadow && namedImport.isTypeOnly) {
                continue;
            }

            // 检查导入名称是否与源节点名称匹配
            if (namedImport.name.getText() === sourceNode.name) {
                return this.reportShadow(
                    targetFile,
                    sourceFile,
                    sourceNode.node,
                    namedImport.name
                );
            }
        }

        return false;
    }

    private reportShadow(targetFile: ArkFile, sourceFile: ts.SourceFileLike, sourceNode: ts.Node, shadowNode: ts.Node): boolean {
        if (sourceNode === shadowNode) {
            return false;
        }

        const sourceStartPosition = ts.getLineAndCharacterOfPosition(sourceFile, sourceNode.getStart());
        const startLine = sourceStartPosition.line + 1;
        const startCol = sourceStartPosition.character + 1;

        const sourceEndPosition = ts.getLineAndCharacterOfPosition(sourceFile, sourceNode.getEnd());
        const sourceEnd = sourceEndPosition.character + 1;

        const shadowStartPosition = ts.getLineAndCharacterOfPosition(sourceFile, shadowNode.getStart());
        const shadowLine = shadowStartPosition.line + 1;
        const shadowCol = shadowStartPosition.character + 1;

        if (startLine === shadowLine && startCol === shadowCol) {
            return false;
        }

        const message = "'" + sourceNode.getText() + "' is already declared in the upper scope on line " + shadowLine + ' column ' + shadowCol + '.';

        this.addIssueReport(targetFile, startLine, startCol, sourceEnd, message);

        return true;
    }

    private addIssueReport(arkFile: ArkFile, line: number, startCol: number, endCol: number, message: string): void {
        const severity = this.rule.alert ?? this.metaData.severity;
        const filePath = arkFile.getFilePath();
        const defect = new Defects(line, startCol, endCol, message, severity, this.rule.ruleId,
            filePath, this.metaData.ruleDocPath, true, false, false);
        this.issues.push(new IssueReport(defect, undefined));
        RuleListUtil.push(defect);
    }
}